"""
《邢不行-2020新版|Python数字货币量化投资课程》
无需编程基础，助教答疑服务，专属策略网站，一旦加入，永续更新。
课程详细介绍：https://quantclass.cn/crypto/class
邢不行微信: xbx9025
本程序作者: 邢不行

# 课程内容
币安u本位择时策略实盘框架相关函数
"""
import ccxt
import traceback
from ccxt.base.errors import OrderNotFound
import math
import functools
import logging
from logging.handlers import TimedRotatingFileHandler
import random
import pandas as pd
import threading
from datetime import datetime, timedelta
import time
from Config import *
import Signals


# ==========辅助功能函数==========
def retry(act_name='', sleep_seconds=1, retry_times=3):
    def _(func):
        @functools.wraps(func)
        def wrapper(*args, **params):
            """
            需要在出错时不断重试的函数，例如和交易所交互，可以使用本函数调用。
            :param act_name: 本次动作的名称
            :param sleep_seconds: 报错后的sleep时间
            :param retry_times: 为最大的出错重试次数
            :return:
            """
            nonlocal retry_times, act_name, sleep_seconds
            e = ''
            for _ in range(retry_times):
                try:
                    result = func(*args, **params)
                    return result
                except Exception as e:
                    time.sleep(sleep_seconds)
            else:
                raise Exception(act_name + f'报错重试次数超过上限，程序退出。错误内容是：{e} \n' + traceback.format_exc())

        return wrapper

    return _


# ===下次运行时间，和课程里面讲的函数是一样的
def next_run_time(time_interval, ahead_seconds=5, logger=None):
    """
    根据time_interval，计算下次运行的时间，下一个整点时刻。
    目前只支持分钟和小时。
    :param time_interval: 运行的周期，15m，1h
    :param ahead_seconds: 预留的目标时间和当前时间的间隙
    :return: 下次运行的时间
    案例：
    15m  当前时间为：12:50:51  返回时间为：13:00:00
    15m  当前时间为：12:39:51  返回时间为：12:45:00
    10m  当前时间为：12:38:51  返回时间为：12:40:00
    5m  当前时间为：12:33:51  返回时间为：12:35:00
    5m  当前时间为：12:34:51  返回时间为：12:35:00

    1h  当前时间为：14:37:51  返回时间为：15:00:00
    2h  当前时间为：00:37:51  返回时间为：02:00:00

    30m  当前时间为：21日的23:33:51  返回时间为：22日的00:00:00
    5m  当前时间为：21日的23:57:51  返回时间为：22日的00:00:00

    ahead_seconds = 5
    15m  当前时间为：12:59:57  返回时间为：13:15:00，而不是 13:00:00
    """
    if not logger:
        logger = logging.getLogger()
    if time_interval.endswith('m') or time_interval.endswith('h'):
        pass
    elif time_interval.endswith('T'):
        time_interval = time_interval.replace('T', 'm')
    elif time_interval.endswith('H'):
        time_interval = time_interval.replace('H', 'h')
    else:
        logger.error('time_interval格式不符合规范。程序exit')
        exit()

    ti = pd.to_timedelta(time_interval)
    now_time = datetime.now()
    # now_time = datetime(2019, 5, 9, 23, 50, 30)  # 指定now_time，可用于测试
    this_midnight = now_time.replace(hour=0, minute=0, second=0, microsecond=0)
    min_step = timedelta(minutes=1)

    target_time = now_time.replace(second=0, microsecond=0)

    while True:
        target_time = target_time + min_step
        delta = target_time - this_midnight
        if delta.seconds % ti.seconds == 0 and (target_time - now_time).seconds >= ahead_seconds:
            # 当符合运行周期，并且目标时间有足够大的余地，默认为60s
            break

    logger.debug('\n程序下次运行的时间：', target_time, '\n')
    return target_time


# ===依据时间间隔, 自动计算并休眠到指定时间
def sleep_until_run_time(time_interval, ahead_time=1, if_sleep=True):
    """
    根据next_run_time()函数计算出下次程序运行的时候，然后sleep至该时间
    :param time_interval:
    :param ahead_time:
    :param if_sleep:
    :return:
    """

    # 计算下次运行时间
    run_time = next_run_time(time_interval, ahead_time)

    # sleep
    if if_sleep:
        time.sleep(max(0, (run_time - datetime.now()).seconds))
        # 可以考察：print(run_time - n)、print((run_time - n).seconds)
        while True:  # 在靠近目标时间时
            if datetime.now() > run_time:
                break

    return run_time


# ===将最新数据和历史数据合并
def symbol_candle_data_append_recent_candle_data(symbol_candle_data, recent_candle_data, symbol_config, max_candle_num):
    for symbol in symbol_config.keys():
        df = symbol_candle_data[symbol].append(recent_candle_data[symbol], ignore_index=True)
        df.drop_duplicates(subset=['candle_begin_time_GMT8'], keep='last', inplace=True)
        df.sort_values(by='candle_begin_time_GMT8', inplace=True)  # 排序，理论上这步应该可以省略，加快速度
        df = df.iloc[-max_candle_num:]  # 保持最大K线数量不会超过max_candle_num个
        df.reset_index(drop=True, inplace=True)
        symbol_candle_data[symbol] = df

    return symbol_candle_data


# ===重试机制
def retry_wrapper(func, params={}, act_name='', sleep_seconds=3, retry_times=5):
    """
    需要在出错时不断重试的函数，例如和交易所交互，可以使用本函数调用。
    :param func: 需要重试的函数名
    :param params: func的参数
    :param act_name: 本次动作的名称
    :param sleep_seconds: 报错后的sleep时间
    :param retry_times: 为最大的出错重试次数
    :return:
    """

    for _ in range(retry_times):
        try:
            result = func(params=params)
            return result
        except Exception as e:
            print(act_name, '报错，报错内容：', str(e), '程序暂停(秒)：', sleep_seconds)
            time.sleep(sleep_seconds)
    else:
        # send_dingding_and_raise_error(output_info)
        raise ValueError(act_name, '报错重试次数超过上限，程序退出。')


# ==========交易所交互函数==========
# ===判断当前持仓模式
def if_oneway_mode(exchange):
    """
    判断当前合约持仓模式。必须得是单向模式。如果是双向模式，就报错。
    查询当前的持仓模式。使用函数：GET /fapi/v1/positionSide/dual (HMAC SHA256)
    判断持仓情况，False为单向持仓，True为单向持仓
    :param exchange:
    :return:
    """

    positionSide = retry_wrapper(exchange.fapiPrivateGetPositionSideDual, act_name='查看合约持仓模式')

    if positionSide['dualSidePosition']:
        raise ValueError("当前持仓模式为双向持仓，程序已停止运行。请去币安官网改为单向持仓。")
    else:
        return '当前持仓模式：单向持仓'


# ===获得币对精度
def usdt_future_exchange_info(exchange, symbol_config):
    """
    获取symbol_config中币种的最小下单价格、数量
    :param exchange:
    :return:
    使用接口：GET /fapi/v1/exchangeInfo
    文档：https://binance-docs.github.io/apidocs/futures/cn/#0f3f2d5ee7
    """

    # 获取u本为合约交易对的信息
    exchange_info = retry_wrapper(exchange.fapiPublic_get_exchangeinfo, act_name='查看合约基本信息')

    # 转化为dataframe
    df = pd.DataFrame(exchange_info['symbols'])
    # df['minPrice'] = df['filters'].apply(lambda x: x[0]['minPrice'])
    # df['minQty'] = df['filters'].apply(lambda x: x[1]['minQty'])
    df['tickSize'] = df['filters'].apply(lambda x: math.log(1 / float(x[0]['tickSize']), 10))
    df['stepSize'] = df['filters'].apply(lambda x: math.log(1 / float(x[1]['stepSize']), 10))
    df = df[['symbol', 'pricePrecision', 'quantityPrecision', 'tickSize', 'stepSize']]
    df.set_index('symbol', inplace=True)

    # 赋值
    for symbol in symbol_config.keys():
        symbol_config[symbol]['最小下单价精度'] = int(df.at[symbol, 'tickSize'])

        p = int(df.at[symbol, 'quantityPrecision'])
        symbol_config[symbol]['最小下单量精度'] = None if p == 0 else p


# ===获取当前持仓信息
def binance_update_account(exchange, symbol_config, symbol_info):
    """
    获取u本位账户的持仓信息、账户余额信息
    :param exchange:
    :param symbol_config:
    :param symbol_info:
    :return:
    接口：GET /fapi/v2/account (HMAC SHA256)
    文档：https://binance-docs.github.io/apidocs/futures/cn/#v2-user_data-2
    币安的币本位合约，不管是交割，还是永续，共享一个账户。他们的symbol不一样。比如btc的永续合约是BTCUSDT，季度合约是BTCUSDT_210625
    """
    # ===获取持仓数据===
    # 获取账户信息
    # account_info = exchange.fapiPrivateGetAccount()
    account_info = retry_wrapper(exchange.fapiPrivateGetAccount, act_name='查看合约账户信息')

    # 将持仓信息转变成dataframe格式
    positions_df = pd.DataFrame(account_info['positions'], dtype=float)
    positions_df = positions_df.set_index('symbol')
    # 筛选交易的币对
    positions_df = positions_df[positions_df.index.isin(symbol_config.keys())]
    # 将账户信息转变成dataframe格式
    assets_df = pd.DataFrame(account_info['assets'], dtype=float)
    assets_df = assets_df.set_index('asset')

    # 根据持仓信息、账户信息中的值填充symbol_info
    balance = assets_df.loc['USDT', 'marginBalance']  # 保证金余额
    symbol_info['账户权益'] = balance

    symbol_info['持仓量'] = positions_df['positionAmt']
    symbol_info['持仓方向'] = symbol_info['持仓量'].apply(lambda x: 1 if float(x) > 0 else (-1 if float(x) < 0 else 0))

    symbol_info['持仓收益'] = positions_df['unrealizedProfit']
    symbol_info['持仓均价'] = positions_df['entryPrice']

    # 计算每个币种的分配资金（在无平仓的情况下）
    profit = symbol_info['持仓收益'].sum()
    symbol_info['分配资金'] = (balance - profit) * symbol_info['分配比例']

    return symbol_info


# ===通过ccxt获取K线数据
def ccxt_fetch_binance_candle_data(exchange, symbol, time_interval, limit):
    """
    获取指定币种的K线信息
    :param exchange:
    :param symbol:
    :param time_interval:
    :param limit:
    :return:
    """

    # 获取数据
    # data = exchange.fapiPublic_get_klines({'symbol': symbol, 'interval': time_interval, 'limit': limit})
    data = retry_wrapper(exchange.fapiPublic_get_klines, act_name='获取币种K线数据',
                         params={'symbol': symbol, 'interval': time_interval, 'limit': limit})

    # 整理数据
    df = pd.DataFrame(data, dtype=float)
    df.rename(columns={1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume'}, inplace=True)
    df['candle_begin_time'] = pd.to_datetime(df[0], unit='ms')
    df['candle_begin_time_GMT8'] = df['candle_begin_time'] + timedelta(hours=8)
    df = df[['candle_begin_time_GMT8', 'open', 'high', 'low', 'close', 'volume']]

    return df


# ===单线程获取需要的K线数据，并检测质量。
def single_threading_get_binance_candle_data(exchange, symbol_config, symbol_info, time_interval, run_time, candle_num, logger):
    """
    获取所有币种的k线数据，并初步处理
    :param exchange:
    :param symbol_config:
    :param symbol_info:
    :param time_interval:
    :param run_time:
    :param candle_num:
    :return:
    """

    symbol_candle_data = dict()  # 用于存储K线数据

    logger.debug('开始获取K线数据')
    # 遍历每一个币种
    for symbol in symbol_config.keys():
        logger.debug(symbol + '开始时间：' + str(datetime.now()))

        # 获取symbol该品种最新的K线数据
        df = ccxt_fetch_binance_candle_data(exchange, symbol, time_interval, limit=candle_num)

        # 如果获取数据为空，再次获取
        # if df.empty:
        # continue

        # 获取到了最新数据
        logger.debug('结束时间：' + str(datetime.now()))
        symbol_info.at[symbol, '当前价格'] = df.iloc[-1]['close']  # 该品种的最新价格
        symbol_candle_data[symbol] = df[df['candle_begin_time_GMT8'] < pd.to_datetime(run_time)]  # 去除run_time周期的数据

    return symbol_candle_data


# ===获取需要的币种的历史K线数据。
def get_binance_history_candle_data(exchange, symbol_config, time_interval, candle_num, logger, if_print=True):
    symbol_candle_data = dict()  # 用于存储K线数据
    logger.debug('获取交易币种的历史K线数据')

    # 遍历每一个币种
    for symbol in symbol_config.keys():

        # 获取symbol该品种最新的K线数据
        df = ccxt_fetch_binance_candle_data(exchange, symbol, time_interval, limit=candle_num)

        # 为了保险起见，去掉最后一行最新的数据
        df = df[:-1]

        symbol_candle_data[symbol] = df  # 去除run_time周期的数据
        time.sleep(medium_sleep_time)

        if if_print:
            logger.debug(symbol)
            logger.debug(symbol_candle_data[symbol].tail(3))

    return symbol_candle_data


# ===批量下单
def place_binance_batch_order(exchange, symbol_order_params):
    num = 5  # 每个批量最多下单的数量
    for i in range(0, len(symbol_order_params), num):
        order_list = symbol_order_params[i:i + num]
        params = {'batchOrders': exchange.json(order_list),
                  'timestamp': int(time.time() * 1000)}
        # order_info = exchange.fapiPrivatePostBatchOrders(params)
        order_info = retry_wrapper(exchange.fapiPrivatePostBatchOrders, params=params, act_name='批量下单')

        print('\n成交订单信息\n', order_info)
        time.sleep(short_sleep_time)


def place_binance_batch_order_twap(exchange, symbol_order_params, symbol_config, logger):
    pool = []
    for item in symbol_order_params:
        pool.append(Twap(exchange, item, symbol_config, logger=logger))
    for item in pool:
        item.start()
    return pool


# ==========趋势策略相关函数==========
def calculate_signal(symbol_info, symbol_config, symbol_candle_data):
    """
    计算交易信号
    :param symbol_info:
    :param symbol_config:
    :param symbol_candle_data:
    :return:
    """
    # return变量
    symbol_signal = {
        '平多': [],
        '平空': [],
        '开多': [],
        '开空': [],
        '平多开空': [],
        '平空开多': [],
    }

    # 逐个遍历交易对
    for symbol in symbol_config.keys():

        # 赋值相关数据
        df = symbol_candle_data[symbol].copy()  # 最新数据
        now_pos = symbol_info.at[symbol, '持仓方向']  # 当前持仓方向
        avg_price = symbol_info.at[symbol, '持仓均价']  # 当前持仓均价

        # 需要计算的目标仓位
        target_pos = None

        # 根据策略计算出目标交易信号。
        if not df.empty:  # 当原始数据不为空的时候
            target_pos = getattr(Signals, symbol_config[symbol]['strategy_name'])(df, now_pos, avg_price,
                                                                                  symbol_config[symbol]['para'])
            symbol_info.at[symbol, '目标持仓'] = target_pos

        # 根据目标仓位和实际仓位，计算实际操作
        if now_pos == 1 and target_pos == 0:  # 平多
            symbol_signal['平多'].append(symbol)
        elif now_pos == -1 and target_pos == 0:  # 平空
            symbol_signal['平空'].append(symbol)
        elif now_pos == 0 and target_pos == 1:  # 开多
            symbol_signal['开多'].append(symbol)
        elif now_pos == 0 and target_pos == -1:  # 开空
            symbol_signal['开空'].append(symbol)
        elif now_pos == 1 and target_pos == -1:  # 平多，开空
            symbol_signal['平多开空'].append(symbol)
        elif now_pos == -1 and target_pos == 1:  # 平空，开多
            symbol_signal['平空开多'].append(symbol)

        symbol_info.at[symbol, '信号时间'] = datetime.now()  # 计算产生信号的时间

    # 删除没有信号的操作
    for key in list(symbol_signal.keys()):
        if not symbol_signal.get(key):
            del symbol_signal[key]

    return symbol_signal


# 根据交易所的限制（最小下单单位、量等），修改下单的数量和价格
def modify_order_quantity_and_price(symbol, symbol_config, params):
    """
    根据交易所的限制（最小下单单位、量等），修改下单的数量和价格
    :param symbol:
    :param symbol_config:
    :param params:
    :return:
    """

    # 根据每个币种的精度，修改下单数量的精度
    params['quantity'] = round(params['quantity'], symbol_config[symbol]['最小下单量精度'])

    # 买单加价2%，卖单降价2%
    params['price'] = params['price'] * 1.02 if params['side'] == 'BUY' else params['price'] * 0.98
    # 根据每个币种的精度，修改下单价格的精度
    params['price'] = round(params['price'], symbol_config[symbol]['最小下单价精度'])

    return params


def modify_order_quantity_and_price2(symbol, symbol_config, params):
    """
    根据交易所的限制（最小下单单位、量等），修改下单的数量和价格
    :param symbol:
    :param symbol_config:
    :param params:
    :return:
    """

    # 根据每个币种的精度，修改下单数量的精度
    params['quantity'] = round(params['quantity'], symbol_config[symbol]['最小下单量精度'])

    # 买单加价2%，卖单降价2%
    # 根据每个币种的精度，修改下单价格的精度
    params['price'] = round(params['price'], symbol_config[symbol]['最小下单价精度'])

    return params


# 针对某个类型订单，计算下单参数。供cal_all_order_info函数调用
def cal_order_params(signal_type, symbol, symbol_info, symbol_config, logger=None):
    """
    针对某个类型订单，计算下单参数。供cal_all_order_info函数调用
    :param signal_type:
    :param symbol:
    :param symbol_info:
    :param symbol_config:
    :return:
    """
    if not logger:
        logger = logging.getLogger()

    params = {
        'symbol': symbol,
        'side': binance_order_type[signal_type],
        'price': symbol_info.at[symbol, '当前价格'],
        'type': 'LIMIT',
        'timeInForce': 'GTC',
    }

    if signal_type in ['平空', '平多']:
        params['quantity'] = abs(symbol_info.at[symbol, '持仓量'])

    elif signal_type in ['开多', '开空']:
        params['quantity'] = symbol_info.at[symbol, '分配资金'] * symbol_config[symbol]['leverage'] / \
                             symbol_info.at[symbol, '当前价格']

    else:
        close_quantity = abs(symbol_info.at[symbol, '持仓量'])
        open_quantity = symbol_info.at[symbol, '分配资金'] * symbol_config[symbol]['leverage'] / \
                        symbol_info.at[symbol, '当前价格']
        params['quantity'] = close_quantity + open_quantity

    # 修改精度
    logger.debug(symbol + '修改精度前' + str(params))
    params = modify_order_quantity_and_price(symbol, symbol_config, params)
    logger.debug(symbol + '修改精度后' + str(params))

    return params


# 计算所有币种的下单参数
def cal_all_order_info(symbol_signal, symbol_info, symbol_config, exchange, logger):
    """

    :param symbol_signal:
    :param symbol_info:
    :param symbol_config:
    :param exchange:
    :return:
    """

    symbol_order_params = []

    # 如果没有信号，跳过
    if not symbol_signal:
        logger.debug('本周期无交易指令，不执行交易操作')
        return symbol_order_params

    # 如果只有平仓，或者只有开仓，无需重新更新持仓信息symbol_info
    if set(symbol_signal.keys()).issubset(['平空', '平多']) or set(symbol_signal.keys()).issubset(['开多', '开空']):
        logger.debug('本周期只有平仓或者只有开仓交易指令，无需再次更新账户信息，直接执行交易操作')

    # 如果有其他信号，需重新更新持仓信息symbol_info，然后据此重新计算下单量
    else:
        logger.debug('本周期有复杂交易指令（例如：平开、平和开、有平和平开、有开和平开），需重新更新账户信息，再执行交易操作')

        # 更新账户信息symbol_info
        symbol_info = binance_update_account(exchange, symbol_config, symbol_info)

        # 标记出需要把利润算作保证金的仓位。
        for signal in symbol_signal.keys():
            for symbol in symbol_signal[signal]:
                symbol_info.at[symbol, '利润参与保证金'] = 1

        # 计算分配资金
        all_profit = symbol_info['持仓收益'].sum()  # 所有利润
        profit = (symbol_info['持仓收益'] * symbol_info['利润参与保证金']).sum()  # 参与保证金的利润
        balance = symbol_info.iloc[0]['账户权益'] - all_profit  # 初始投入资金
        balance = balance + profit  # 平仓之后的利润或损失
        symbol_info['分配资金'] = balance * symbol_info['分配比例']
        logger.debug('\n更新持仓信息、分配资金信息\n' + str(symbol_info))

    # 计算每个交易币种的各个下单参数
    for signal_type in symbol_signal.keys():
        for symbol in symbol_signal[signal_type]:
            params = cal_order_params(signal_type, symbol, symbol_info, symbol_config, logger=logger)

            if params['quantity'] == 0:  # 考察下单量是否为0
                logger.debug('\n', symbol, '下单量为0，忽略')
            elif params['price'] * params['quantity'] <= 5:  # 和最小下单额5美元比较
                logger.debug('\n', symbol, '下单金额小于5u，忽略')
            else:
                # 改成str
                params['price'] = str(params['price'])
                params['quantity'] = str(params['quantity'])
                symbol_order_params.append(params)

    return symbol_order_params


# @retry(act_name='ping', logger_name='ping')
def ping(exchange):
    r = exchange.fapiPublicGetPing()
    return r


class Twap(threading.Thread):
    """
    拆单，并且目前只做maker的算法单
    初始化后为一个线程对象的子类，直接start即可
    """

    def __init__(self, EXCHANGE, order_info, symbol_config, min_=0.5, max_=2, time_interval=3, place=0.0003, logger=None):
        """
        简单算法单函数，按固定时间随机下单算法。
        order_info 是用于传递给ccxt的binance的json格式的订单参数。示例：
        {'symbol': 'ETHUSDT', 'side': 'SELL', 'price': '1396.6', 'type': 'LIMIT', 'timeInForce': 'GTC', 'quantity': '0.049'}
        min 表示，随机化时最小下单量
        max表示随机化时，最大下单量
        time_interval表示，间隔多久检查一次是否成交
        place 表示，挂单离当前盘口的最新价的距离，距离过近容易导致吃单率上升，过于会比较难成交
        """
        threading.Thread.__init__(self)
        if not logger:
            self.logger = logging.getLogger('algo')
        else:
            self.logger = logger
        self.logger.debug('algo start!')
        self.logger.debug(f'algo order param::{order_info}::{symbol_config}::{min_}::{max_}::{time_interval}::{place}')
        self.EXCHANGE = EXCHANGE
        self.order_info = order_info
        self.param = order_info
        self.param['timeInForce'] = 'GTX'  # 算法单默认下maker单
        self.param['quantity'] = round(float(self.param['quantity']), symbol_config[order_info['symbol']]['最小下单量精度'])
        self.quantity = self.param['quantity']
        self.orders = {}  # 存放所有的本函数运行以来发送的订单
        self.this_order = None  # 表示算法过程中当前未成交的单，如有为订单id，如没有为单号
        self.replaced_order = None  # 当一个子单因为撤单需要重下时，次变量存放重下的子单。
        self.symbol_config = symbol_config # 用来交易所相关的静态值，如最大下单精度，价格精度等等。
        self.time_interval = time_interval
        self.place = place
        self.max_ = max_
        self.min_ = min_

    def run(self):
        # EXCHANGE.fapiPrivateGetAllOrders({'symbol': 'ETHUSDT', 'timestamp': int(time.time() * 1000)})
        while True:
            try:
                ticker = self.fetch_ticker(self.order_info['symbol'])
                vol = round(random.uniform(self.min_, self.max_), self.symbol_config[self.order_info['symbol']]['最小下单量精度'])
                price = self.get_price(ticker)
                if price:
                    self.param['price'] = price
                else:
                    self.logger.error(f'价格为空{price}')

                if not self.this_order:  # 没有挂单的情况
                    if self.replaced_order:  # 如果，有需要重下的订单，进行重下
                        self.replaced_order['price'] = price  # check cancel 内没有填写价格，此时获取了价格再行填写
                        tp = self.post_order(self.replaced_order)
                        self.orders[tp['orderId']] = tp
                        self.this_order = tp['orderId']
                        self.replaced_order = None
                        time.sleep(self.time_interval)
                        continue

                    if vol <= self.quantity and self.quantity -vol > self.min_ * 0.1:  # 小于0.1的十分之一，就直接一起下了，避免剩下的无法下单
                        self.param['quantity'] = vol
                        tp = self.post_order(self.param)
                        self.orders[tp['orderId']] = tp
                        self.this_order = tp['orderId']
                        self.quantity -= vol  # 更新剩余数量
                    else:
                        # 单量太小，这种情况直接下单，不用拆单了
                        self.param['quantity'] = round(self.quantity, self.symbol_config[self.order_info['symbol']]['最小下单量精度'])
                        self.quantity = 0
                        tp = self.post_order(self.param)
                        self.orders[tp['orderId']] = tp
                        self.this_order = tp['orderId']
                else:
                    param_get = {"symbol": self.order_info['symbol'], 'orderId': self.this_order,
                                 'timestamp': int(time.time() * 1000)}
                    res = self.get_order(param_get)
                    if res['status'] == 'FILLED':  # 已经成交
                        self.this_order = None
                        if self.quantity == 0:
                            # 循环到所有的quantity都下完，就退出
                            # 需要等到所有的条件都判断完，后才能判断是否退出，否则，存在，最后一单被撤了，但是还需要继续下单等情况，却直接 退出的问题
                            break
                    elif res['status'] == 'NEW' or res['status'] == 'PARTIALLY_FILLED':  # 还未成交。
                        if self.order_info['side'] == 'BUY':
                            if price > float(res['price']):  # 不是最优价格就撤单
                                self.cancel_order(param_get)
                                self.check_cancel(param_get)

                        elif self.order_info['side'] == 'SELL':
                            if price < float(res['price']):  # 不是最优价格就撤单
                                self.cancel_order(param_get)
                                self.check_cancel(param_get)
                        else:
                            self.logger.error(f'异常的交易方向！{self.order_info}')
                            raise Exception('交易方向异常！')

                    elif res['status'] == 'EXPIRED':  # 只做maker，行情急变，导致被撤单。
                        self.replaced_order = {'symbol': res['symbol'],
                                          'side': res['side'],
                                          'price': '',
                                          'type': res['type'],
                                          'timeInForce': res['timeInForce'],
                                          'quantity': float(res['origQty'])}
                        self.replaced_order['quantity'] = round(self.replaced_order['quantity'],
                                                           self.symbol_config[self.order_info['symbol']]['最小下单量精度'])
                        self.this_order = None  # 当前挂单已经失效。
                    else:
                        self.logger.error(f'异常的订单状态！{self.order_info}')
                        raise Exception('订单状态异常！')

                time.sleep(self.time_interval)
            except Exception as e:
                self.logger.error('Twap运行出错，出错原因：' + traceback.format_exc())
                return

    @retry('get order')
    def get_order(self, param_get):
        ord = self.EXCHANGE.fapiPrivateGetOrder(params=param_get)
        self.logger.debug(f'查订单结果：{ord}')
        return ord

    @retry('post order')
    def post_order(self, params):
        self.logger.debug(f'下单：{params}')
        return self.EXCHANGE.fapiPrivatePostOrder(params=params)

    @retry('cancel order')
    def cancel_order(self, param_get):
        self.logger.debug(f'撤单：{param_get}')
        try:
            self.EXCHANGE.fapiPrivateDeleteOrder(params=param_get)
        except OrderNotFound:
            self.logger.warning(f'要撤的单已经成交了！{param_get}')

    @retry('fetch_ticker')
    def fetch_ticker(self, symbol):
        ticker = self.EXCHANGE.fapiPublicGetTickerBookTicker({"symbol": symbol})
        self.logger.debug(f'获取ticker：{ticker}')
        return ticker

    def check_cancel(self, param_get):
        for i in range(3):
            time.sleep(1)  # 等一秒看是否撤了，或成交了
            r = self.get_order(param_get)
            if r['status'] == 'CANCELED':
                self.this_order = None
            elif r['status'] == 'FILLED':  # 在之前撤单到达之前就已经成交了。防错机制。
                self.this_order = None
            elif r['status'] == 'NEW' or r['status'] == 'PARTIALLY_FILLED':
                # 撤单未被ba处理，再撤一次。
                self.cancel_order(param_get)
                self.logger.warning(f'订单撤单发送成功后，没有被撤掉,再撤一次：{param_get}')
                continue
            else:
                self.logger.error(f'出现意外的订单状态！{r}')
                raise Exception(f'出现意外的订单状态！{r}')
            if not self.replaced_order:
                self.replaced_order = {'symbol': r['symbol'],
                                  'side': r['side'],
                                  'price': '',
                                  'type': r['type'],
                                  'timeInForce': r['timeInForce'],
                                  'quantity': float(r['origQty']) - float(r['executedQty'])}  # 确保部分成交正确处理
            else:
                self.logger.error('撤单后replace order不为空！')
            self.replaced_order['quantity'] = round(self.replaced_order['quantity'],
                                               self.symbol_config[self.order_info['symbol']]['最小下单量精度'])
            break
        else:  # 撤了三次还没撤掉，报错
            raise Exception('check cancel循环三次还有未撤订单，请检查！')

    def get_price(self, ticker):
        if self.order_info['side'] == 'BUY':
            price_ = float(ticker['bidPrice'])
        elif self.order_info['side'] == 'SELL':
            price_ = float(ticker['askPrice'])
        else:
            self.logger.error(f'异常的交易方向！{self.order_info}')
            raise Exception('交易方向异常！')

        return round(price_, self.symbol_config[self.order_info['symbol']]['最小下单价精度'])


if __name__ == '__main__':
    logging.basicConfig(format="%(asctime)s-%(name)s-%(levelname)s-%(message)s-%(lineno)d-%(threadName)s-", level=logging.DEBUG)
    symbol_config = {
        # 'ETHUSDT': {'leverage': 1,
        #                  'strategy_name': 'real_signal_simple_bolling',
        #                  'para': [660, 1],
        #                  'position': 1,
        #                  },
        'ETHUSDT': {'leverage': 1,
                    'strategy_name': 'real_signal_random',
                    'para': [660, 1],
                    'position': 0.1,
                    },
    }
    l = logging.getLogger('test')
    l.setLevel(logging.ERROR)
    BINANCE_CONFIG = {
        'apiKey': 'e20828531d57bd2a77280283bd719ccd6fa1428f3f77f20e2685c071bbdf7260',
        'secret': 'e9da80281c31e14cc655c32fc996d24d6881eda9df85903acab79901c6c96c8d',
        'timeout': 3000,
        'rateLimit': 10,
        'verbose': False,
        'hostname': 'https://testnet.binancefuture.com',
        'enableRateLimit': False,
        'proxies': {'https': "http://127.0.0.1:7890", 'http': "http://127.0.0.1:7890"},
        'logger': l
    }

    EXCHANGE = ccxt.binance(BINANCE_CONFIG)
    EXCHANGE.set_sandbox_mode(True)
    usdt_future_exchange_info(EXCHANGE, symbol_config)
    od = {'symbol': 'ETHUSDT',
          'side': 'SELL',
          # 'price': '1396.6',
          'type': 'LIMIT',
          'timeInForce': 'GTX',  # 只做maker
          'quantity': 20}
    MARK = 4
    t = Twap(EXCHANGE, od,symbol_config)
    t.start()
    t.join()

